/*
 * File     : StringPoolImpl.java
 * Created  : 7 May 2011
 *
 * Copyright © 2011 Matthew Wilson (mj. {my-surname} .uk {at} gmail.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.dni.internal.string;

import static com.googlecode.dni.internal.DniInternal.PLATFORM;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;

import com.googlecode.dni.DirectNativeInterface;
import com.googlecode.dni.Library;
import com.googlecode.dni.internal.DniInternal;
import com.googlecode.dni.type.NativeObject;
import com.googlecode.dni.type.NativeString;
import com.googlecode.dni.type.Pointer;
import com.googlecode.dni.type.Region;
import com.googlecode.dni.type.StringPool;

/**
 * TODO Class description.
 *
 * @author Matthew Wilson
 */
// TODO THIS IS HALF BROKEN!  (or maybe even 100% broken)
public final class StringPoolImpl implements StringPool
{

    private final FastCharset fastCharset;

    private final int chunkSize;
    
    private final int maximumSegmentCount;

    private final Object lock = new Object();
    
    private final Map< String, PoolNativeString > stringMap = new ConcurrentHashMap< String, PoolNativeString >();

    private final List< Segment > segments = new ArrayList< Segment >();

    private int nextOffset = 0;

    private Region currentRegion = null;

    private ByteBuffer currentBuffer = null;


    public StringPoolImpl( final String charset,
                           final int chunkSize,
                           final int maximumSize )
    {
        this.fastCharset = FastCharset.getInstance( charset );
        this.chunkSize = roundSize( chunkSize );
        this.maximumSegmentCount = Math.max( this.chunkSize, roundSize( maximumSize ) ) / this.chunkSize;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public NativeString get( final String string )
    {
        if ( string == null )
        {
            return null;
        }

        // TODO fast path for zero-length string -- universal constant

        PoolNativeString nativeString = this.stringMap.get( string );
        if ( nativeString != null )
        {
            nativeString.incRefCount();
            return nativeString;
        }
        synchronized ( this.lock )
        {
            int byteCount = this.fastCharset.getByteCount( string );
            if ( byteCount > this.chunkSize || !initBufferForString( byteCount ) )
            {
                // too big to cache or too full
    //                return new AllocatedNativeString( string, byteCount );
                throw new AssertionError();
            }
    
            int actualByteCount = this.fastCharset.encode( string, this.currentBuffer );
            assert actualByteCount <= byteCount;
    
            // create the string
            nativeString = new PoolNativeString( this.currentRegion.getAddress() + this.nextOffset,
                                                 actualByteCount,
                                                 string );
    
            // update the pointer for the next time
            this.nextOffset += actualByteCount;
    
            // cache it
            this.stringMap.put( string, nativeString );
    
            nativeString.incRefCount();
            return nativeString;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void release( final NativeString nativeString )
    {
        PoolNativeString poolNativeString = (PoolNativeString) nativeString;
        if ( poolNativeString.decRefCount() )
        {
            throw new UnsupportedOperationException( "IMPLEMENT ME" );
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void drop( final String string )
    {
        PoolNativeString nativeString = this.stringMap.get( string );
        if ( nativeString != null )
        {
            nativeString.markForDeletion();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void free()
    {
        synchronized ( this.stringMap )
        {
            for ( Segment segment : this.segments )
            {
                Region region = segment.region;
                PLATFORM.unallocatePages( region.pointer(), region.getSize() );
            }

            this.stringMap.clear();
            this.segments.clear();
            this.nextOffset = 0;
            this.currentRegion = null;
            this.currentBuffer = null;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString()
    {
        return "StringPool" + this.segments;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void finalize() throws Throwable
    {
        try
        {
            free();
        }
        catch ( Throwable throwable )
        {
            DniInternal.LOGGER.log( Level.SEVERE,
                                    "Failed to finalize: " + toString(),
                                    throwable );
        }
    }

    private static int roundSize( final int size )
    {
        int pageSize = PLATFORM.getPageSize();
        if ( size < pageSize )
        {
            return pageSize;
        }

        return pageSize * ( ( size + PLATFORM.getPageSize() - 1 ) / PLATFORM.getPageSize() );
    }

    /**
     * Sets up a buffer reading for the given number of bytes.
     *
     * @param byteCount
     *            the number of bytes needed
     *
     * @return <code>true</code> if successful;
     *         <code>false</code> if memory cannot be allocated
     */
    private boolean initBufferForString( final int byteCount )
    {
        if ( this.currentBuffer != null )
        {
            this.currentBuffer.position( this.nextOffset );
            if ( this.currentBuffer.remaining() >= byteCount )
            {
                // already got space
                return true;
            }
        }

        // not got enough space, or not got any buffers

        // - do we have space to allocate more?
        if ( allocateSegment() )
        {
            return true;
        }

        // - can we scavenge?

        // TODO implement scavenging
        return false;
    }

    /**
     * Allocates more memory, if possible.
     *
     * @return <code>true</code> iff successful
     */
    private boolean allocateSegment()
    {
        // honour maximum size
        if ( this.segments.size() >= this.maximumSegmentCount )
        {
            return false;
        }

        Pointer basePointer = PLATFORM.allocatePages( this.chunkSize, false );
        Region region = new Region( basePointer, this.chunkSize );
        Segment segment = new Segment( region, region.getBuffer() );

        this.segments.add( segment );

        this.nextOffset = 0;
        this.currentRegion = segment.region;
        this.currentBuffer = segment.buffer;

        return true;
    }


    /**
     * Implementation of {@link NativeString} that is pooled.
     */
    public static final class PoolNativeString implements NativeString
    {

        private final AtomicInteger refCount = new AtomicInteger();

        private final Pointer pointer;

        private final int length;

        private final String string;
        

        /**
         * @param pointer
         *            the pointer to the string
         * @param length
         *            the length of the memory (including null terminator)
         * @param string
         *            the Java string
         */
        PoolNativeString( final long pointer, final int length, final String string )
        {
            this.pointer = new Pointer( pointer );
            this.length = length;
            this.string = string;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Pointer pointer()
        {
            return this.pointer;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean isSameObject( final NativeObject other )
        {
            return other instanceof NativeString && other.pointer().equals( this.pointer );
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void free()
        {
            // can't do anything
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public String read( final String charset )
        {
            return this.string;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public String toString()
        {
            return "NativeString[" + this.pointer + "]";
        }

        void incRefCount()
        {
            int original;
            int update;
            do
            {
                original = this.refCount.get();
                if ( DniInternal.DEBUG_MODE && original == 0x40000000 )
                {
                    throw new AssertionError( "Someone's failed to release pooled strings" );
                }
                update = ( original & 0x7fffffff ) + 1;
            }
            while ( refCount.weakCompareAndSet( original, update ) );
        }
        
        boolean decRefCount()
        {
            int count = refCount.decrementAndGet();
            return count == 0x80000000;
        }
        
        boolean markForDeletion()
        {
            int original;
            int update;
            do
            {
                original = this.refCount.get();
                if ( ( original & 0x80000000 ) != 0 )
                    return original == 0x80000000;
                
                update = original | 0x80000000;
            }
            while ( refCount.weakCompareAndSet( original, update ) );
            
            return update == 0x80000000;
        }

    }

    private static final class Segment
    {
        final Region region;

        final ByteBuffer buffer;

        /**
         * @param region
         *            the region
         * @param buffer
         *            the buffer
         */
        Segment( final Region region, final ByteBuffer buffer )
        {
            this.region = region;
            this.buffer = buffer;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public String toString()
        {
            return this.region.toString();
        }
    }

    /**
     *
     * @param args command-line arguments
     */
    public static void main( final String[] args )
    {
        StringPool pool = new StringPoolImpl( "##default##", 4096, 4096 * 2 );

        System.out.println( pool.get( "Hello, World!" ) );
        System.out.println( pool.get( "Hello, World!" ) );
        System.out.println( pool.get( "Hello, World?!!" ) );


        Moo.INSTANCE.puts( pool.get( "Hello, World!" ) );

        String s = "";
        for ( int i = 0; i < 10; i++ )
        {
            pool.release( pool.get( s ) );
            test( pool, s );
            s += "0123456789";
        }
    }

    private static void test( final StringPool pool, final String string )
    {
        int n = 100000000;
        long time = System.nanoTime();

        for ( int i = 0; i< n ; i++ )
        {
            pool.release( pool.get( string ) );
        }

        time = System.nanoTime() - time;
        System.out.println( time / (double) n );
    }


    @Library
    private interface Moo
    {
        Moo INSTANCE = DirectNativeInterface.getLibrary( Moo.class );
        int puts( NativeString s );
    };

}
